Henley Passport DatasetVisa Requirements by Country of Origin

Henley Passport Dataset

Author

Kevin Valdivia

Published

September 9, 2025

Thanks to Brenden Smith and Jen Richmond from the tidytuesday community for curating this week’s 2025 dataset from the Henley Passport Index, which measures the number of countries each passport allows its holders to enter visa-free, including visa-on-arrival and electronic travel authorization (ETA) access.

Library

The following are the libraries used in the analysis.

library(tidyverse)
library(jsonlite)

# Plotting
library(plotly)
library(sf)
library(rnaturalearth)
library(rnaturalearthdata)

Data Importation

Importing the data from the tidytuesday repo

country_lists = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-09-09/country_lists.csv")
rank_by_year = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-09-09/rank_by_year.csv")

Data Manipulation

We first clean the data to get it into a tidy format. Specifically, we want to create from and to columns to capture the direction of travel, along with the visa requirements for that direction. This structure will make it easier to analyze and visualize visa connections between countries.

travel_perms_no_domesitc <- country_lists |> 
  pivot_longer(
    cols = c(visa_required, visa_online, visa_on_arrival, 
             visa_free_access, electronic_travel_authorisation),
    names_to = "visa_type",
    values_to = "json_str") |> 
  mutate(json_parsed = map(json_str, ~ fromJSON(.)[[1]])) |> 
  unnest(json_parsed, names_sep = "_") |> 
  select(-json_str) |> 
  select(from = code, from_name = country,
         to = json_parsed_code, to_name = json_parsed_name,
         visa_type) |> 
  mutate(visa_type = case_when(
    visa_type %in% c("visa_required",
                   "visa_online",
                   "visa_on_arrival") ~ "Visa Required",
    visa_type %in% c("visa_free_access",
                     "electronic_travel_authorisation") ~ "Easy Access")) |> 
  mutate(from = ifelse(from_name == "Namibia", "NA", from)) |> 
  distinct()
travel_perms_no_domesitc |> head(3)
# A tibble: 3 × 5
  from  from_name             to    to_name     visa_type    
  <chr> <chr>                 <chr> <chr>       <chr>        
1 PS    Palestinian Territory AF    Afghanistan Visa Required
2 PS    Palestinian Territory DZ    Algeria     Visa Required
3 PS    Palestinian Territory AD    Andorra     Visa Required

Before continuing, we need to ensure that the dataset includes self-to-self observations, so that each country’s visa connections include itself. This way, the country of origin will also appear on the map.

home_countries <- travel_perms_no_domesitc |> 
  select(from, from_name) |> 
  distinct() |> 
  mutate(visa_type = "Domestic Travel",
         to = from,
         to_name = from_name)

travel_perms <- travel_perms_no_domesitc |> rbind(home_countries)

Graphing

First, we will join our visa data with the mapping (geospatial) data to prepare it for visualization.

world <- ne_countries(scale = "medium", returnclass = "sf")
world_data <- world |> 
  left_join(travel_perms, by = c("iso_a2_eh" = "to"))

I will now create a function that generates the visa requirements map for any country. The function takes two parameters: the country code to filter the data and the demonym for the country, which is used in the graph title. This approach is flexible and would also be very useful if I wanted to integrate it into a Shiny app.

#' Create an interactive visa requirements map for a given country
#'
#' @param code Character. ISO country code to filter the visa data
#' @param demonym Character. Nationality used in the graph title
#' 
#' @return A Plotly interactive map
#' @example plot_visa_connections("MX", "Mexicans")
#' 
plot_visa_connections <- function(code, demonym) {
  plot_ly() |> 
    add_sf(
      data = world_data |> 
        filter(from == code),
      split = ~iso_a2_eh,
      color = ~visa_type,
      colors = c(
        "Domestic Travel" = "#1f77b4",
        "Easy Access" = "#2ca02c",
        "Visa Required" = "#d62728"
      ),
      text = ~paste("<b>Country:</b>", to_name, 
                    "<b><br>Visa:</b>", visa_type),
      hoveron = "fills",
      hoverinfo = "text" 
    ) |> 
    layout(title = paste0("<br>Visa Connections For <b>", 
                          demonym, "</b>"),
           showlegend = FALSE)
}

Plotting

The following graph shows the visa requirements for entry based on your country of origin. “Easy Access” countries include those that allow either visa-free travel or entry via an electronic travel authorization (ETA) for the specified nationality. “Visa Required” indicates countries where travelers must obtain a visa before entry.

plot_visa_connections("MX", "Mexicans")
plot_visa_connections("CA", "Canadians")
plot_visa_connections("BR", "Brazilians")